import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Hashtable;

/*
 * Class Lexer
 * 
 * Update the method nextToken() such to the provided
 * specifications of the Decaf Programming Language.
 * 
 * You are not allowed to use any built it in tokenizer
 * in Java. You are only allowed to scan the input file
 * one character at a time.
 */

public class Lexer {

	private BufferedReader reader; // Reader
	private char curr; // The current character being scanned

	private static final char EOF = (char) (-1); // End of file character
	private Hashtable<String, Token> Table; // A Hashtable for the Reserved words


	public Lexer(String file) {
		try {
			reader = new BufferedReader(new FileReader(file));
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		
		// Build the Hashtable
		InitialiseTable();
		// Read the first character
		curr = read();
	}

	private void InitialiseTable() {
		Table = new Hashtable<String, Token>();
		// Adding Key Words
		Table.put("class", new Token("KW", "class"));
		Table.put("else", new Token("KW", "else"));
		Table.put("if", new Token("KW", "if"));
		Table.put("int", new Token("KW", "int"));
		Table.put("float", new Token("KW", "float"));
		Table.put("boolean", new Token("KW", "boolean"));
		Table.put("String", new Token("KW", "String"));
		Table.put("return", new Token("KW", "return"));
		Table.put("static", new Token("KW", "static"));
		Table.put("while", new Token("KW", "while"));
		// Adding Booleans
		Table.put("true", new Token("BL", "true"));
		Table.put("false", new Token("BL", "false"));
	}

	private char read() {
		try {
			return (char) (reader.read());
		} catch (IOException e) {
			e.printStackTrace();
			return EOF;
		}
	}

	// Checks if a character is a digit
	private boolean isNumeric(char c) {
		if (c >= '0' && c <= '9')
			return true;

		return false;
	}

	public Token nextToken() {

		int state = 1; // Initial state
		int numBuffer = 0; // A buffer for number literals
		float floatBuffer = 0; // A buffer for float literals
		int decimalPoints = 1; // position of the decimal point
		String strBuffer = ""; // A buffer for string literals
		String wordBuffer = ""; // A buffer for Identifiers/Reserved words
		boolean lookingForRestOfSomething = false; // A flag to handle EOF case

		while (true) {
			if (curr == EOF) {
				if(!lookingForRestOfSomething)
					try {
						reader.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				else
					return new Token("ERROR", "String Literals: " + strBuffer + "EOF");
				
				return null;
			}
				

			switch (state) {
			// Controller
			case 1:
				switch (curr) {
				case ' ': // Whitespaces
				case '\b':
				case '\f':
				case '\n':
				case '\r':
				case '\t':
					curr = read();
					continue;

				case ';':
					curr = read();
					return new Token("SM", ";");

				case '+':
					curr = read();
					return new Token("PO", "+");

				case '-':
					curr = read();
					return new Token("MO", "-");

				case '*':
					curr = read();
					return new Token("TO", "*");
					
				case ',':
					curr = read();
					return new Token("FA", ",");
					
				case '(':
					curr = read();
					return new Token("LP", "(");
					
				case ')':
					curr = read();
					return new Token("RP", ")");
					
				case '{':
					curr = read();
					return new Token("LB", "{");
					
				case '}':
					curr = read();
					return new Token("RB", "}");
					
				case '%':
					curr = read();
					return new Token("MD", "%");
					
				case '=':
					curr = read();
					state = 4;
					continue;
					
				case '!':
					curr = read();
					state = 5;
					continue;
					
				case '|':
					curr = read();
					state = 6;
					continue;
					
				case '&':
					curr = read();
					state = 7;
					continue;

				case '/':
					curr = read();
					state = 8;
					continue;

				case '"':
					curr = read();
					state = 14;
					lookingForRestOfSomething = true;
					continue;

				default:
					state = 2; // Check the next possibility
					continue;
				}

				// Integer - Start
			case 2:
				if (isNumeric(curr)) {
					numBuffer = 0; // Reset the buffer.
					numBuffer += (curr - '0');
					
					state = 3;
					curr = read();
					
					if(curr == EOF)
						return new Token("NM", "" + numBuffer);
					
				} else {
					state = 15;
					continue;
					//return new Token("ERROR", "Invalid input: " + curr);
					// TODO: FIX This!! state = 4; 
				}
				continue;

				// Integer - Body
			case 3:
				if (isNumeric(curr)) {
					numBuffer *= 10;
					numBuffer += (curr - '0');

					curr = read();
				}
				else if(curr == '.') {
					floatBuffer = numBuffer;
					decimalPoints = 1;
					
					state = 12;
					curr = read();
					continue;
				}
				else {
					return new Token("NM", "" + numBuffer);
				}
				continue;
				
				// Equal Operators
			case 4:
				if(curr == '=') {
					curr = read();
					return new Token("EQ", "==");
				}
				else {
					curr = read();
					return new Token("AO", "=");
				}

				
				// NotEqual Operator
			case 5:
				if(curr == '='){
					curr = read();
					return new Token("NE", "!=");
				}
				else {
					curr = read();
					return new Token("ERROR", "Invalid input: !");
				}
				
				// OR Operator
			case 6:
				if(curr == '|'){
					curr = read();
					return new Token("LO", "||");
				}
				else {
					curr = read();
					return new Token("ERROR", "Invalid input: |");
				}
				
				// AND Operator
			case 7:
				if(curr == '&'){
					curr = read();
					return new Token("LA", "&&");
				}
				else {
					curr = read();
					return new Token("ERROR", "Invalid input: &");
				}
				
				// One line comment - Multi line comment - Slash case
			case 8:
				if(curr == '/') {
					curr = read();
					state = 9;
					continue;
				}
				else if(curr == '*') {
					curr = read();
					state = 10;
					continue;
				}
				else {
					curr = read();
					return new Token("DO", "/");
				}
				
				// Ignore entire line till its end
			case 9:
				if(curr == '\n')
					state = 1;
				curr = read();
				continue;
				
				// Ignore text till le wild '*' appears
			case 10:
				if(curr == '*')
					state = 11;
				curr = read();
				continue;
				
				// Stop ignoring if '/' and continue ignoring otherwise
			case 11:
				if(curr == '/')
					state = 1;
				else
					state = 10;
				curr = read();
				continue;
				
			case 12:
				if (isNumeric(curr)) {
					state = 13;
					continue;
				}
				else {
					curr = read();
					return new Token("ERROR", "Invalid input: " + numBuffer + ".");
				}
				
			case 13:
				if (isNumeric(curr)) {
					floatBuffer += ((curr - '0')/(10.0*decimalPoints));
					decimalPoints++;

					curr = read();
					
					if(curr == EOF)
						return new Token("NM", "" + floatBuffer);
				}
				else {
					////curr = read();
					return new Token("NM", "" + floatBuffer);
				}
				continue;
				
				// String
			case 14:
				if(curr == '"') {
					curr = read();
					return new Token("ST", "\"" + strBuffer + "\"");
				}
				else if(curr == '\n') {
					curr = read();
					return new Token("ERROR", "Invalid String Literals: " + strBuffer + "\\n");
				}
					
				strBuffer += curr;
				curr = read();
				continue;
				
				// Identifier - Reserved word beginning
			case 15:
				if(isLetterOrUnderscore(curr)) {
					wordBuffer = "" + curr;
					curr = read();
					state = 16;
					continue;
				}
				else {
					Token tmp = new Token("ERROR", "Invalid input: " + curr);
					curr = read();
					return tmp;
				}
					
				// Identifier - Reserved word body
			case 16:
				if(isLetterOrUnderscore(curr) || isNumeric(curr)) {
					wordBuffer += curr;
					curr = read();
					if(curr == EOF)///
						if(Table.containsKey(wordBuffer))
							return Table.get(wordBuffer);
						else
							return new Token("ID", wordBuffer);
					continue;
				}
				else {
					///curr = read();
					
					if(Table.containsKey(wordBuffer))
						return Table.get(wordBuffer);
					else
						return new Token("ID", wordBuffer);
				}
			}
		}
	}

	private boolean isLetterOrUnderscore(char curr) {
		if(curr>='a' && curr<='z')
			return true;
		if(curr>='A' && curr<='Z')
			return true;
		if(curr=='_')
			return true;
		return false;
	}
}
